/*
 * @(#)TimeZone_md.c	1.18 06/10/10
 *
 * Copyright  1990-2008 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "TimeZone_md.h"

#define VALUE_UNKNOWN		0
#define VALUE_KEY		1
#define VALUE_MAPID		2
#define VALUE_GMTOFFSET		3

#define MAX_ZONE_CHAR		128
#define MAX_MAPID_LENGTH	16

#define NT_TZ_KEY		"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
#define WIN_TZ_KEY		"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Time Zones"
#define NT_CURRENT_TZ_KEY	"SYSTEM\\ControlSet001\\Control\\TimeZoneInformation"
#define WIN_CURRENT_TZ_KEY	"System\\CurrentControlSet\\Control\\TimeZoneInformation"

typedef struct _TziValue {
    LONG	bias;
    LONG	stdBias;
    LONG	dstBias;
    SYSTEMTIME	stdDate;
    SYSTEMTIME	dstDate;
} TziValue;

/*
 * Registry key names
 */
static void *keyNames[] = {
    (void *) L"StandardName",
    (void *) "StandardName",
    (void *) L"Std",
    (void *) "Std"
};

/*
 * Indices to keyNames[]
 */
#define STANDARD_NAME		0
#define STD_NAME		2

static int isNT = FALSE;	/* TRUE if it is NT. */

/*
 * Calls RegQueryValueEx() to get the value for the specified key. If
 * the platform is NT, 2000 or XP, it calls the Unicode
 * version. Otherwise, it calls the ANSI version and converts the
 * value to Unicode. In this case, it assumes that the current ANSI
 * Code Page is the same as the native platform code page (e.g., Code
 * Page 932 for the Japanese Windows systems.
 *
 * `keyIndex' is an index value to the keyNames in Unicode
 * (WCHAR). `keyIndex' + 1 points to its ANSI value.
 *
 * Returns the status value. ERROR_SUCCESS if succeeded, a
 * non-ERROR_SUCCESS value otherwise.
 */
static LONG
getValueInRegistry(HKEY hKey,
		   int keyIndex,
		   LPDWORD typePtr,
		   LPBYTE buf,
		   LPDWORD bufLengthPtr)
{
    LONG ret;
    DWORD bufLength = *bufLengthPtr;
    char val[MAX_ZONE_CHAR];
    DWORD valSize;
    int len;

    *typePtr = 0;
    if (isNT) {
	ret = RegQueryValueExW(hKey, (WCHAR *) keyNames[keyIndex], NULL,
			       typePtr, buf, bufLengthPtr);
	if (ret == ERROR_SUCCESS && *typePtr == REG_SZ) {
	    return ret;
	}
    }

    valSize = sizeof(val);
    ret = RegQueryValueExA(hKey, (char *) keyNames[keyIndex + 1], NULL,
			   typePtr, val, &valSize);
    if (ret != ERROR_SUCCESS) {
	return ret;
    }
    if (*typePtr != REG_SZ) {
	return ERROR_BADKEY;
    }

    len = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS,
			      (LPCSTR) val, -1,
			      (LPWSTR) buf, bufLength/sizeof(WCHAR));
    if (len <= 0) {
	return ERROR_BADKEY;
    }
    return ERROR_SUCCESS;
}

/*
 * Gets the current time zone entry in the "Time Zones" registry.
 */
static int getWinTimeZone(char *winZoneName, char *winMapID)
{
    TIME_ZONE_INFORMATION tzi;
    OSVERSIONINFO ver;
    int onlyMapID;
    HANDLE hKey = NULL, hSubKey = NULL;
    LONG ret;
    DWORD nSubKeys, i;
    ULONG valueType;
    TCHAR szStdName[MAX_ZONE_CHAR];
    TCHAR subKeyName[MAX_ZONE_CHAR];
    TCHAR szValue[MAX_ZONE_CHAR];
    WCHAR stdNameInReg[MAX_ZONE_CHAR];
    TziValue tempTzi;
    WCHAR *stdNamePtr = tzi.StandardName;
    DWORD valueSize;

    /*
     * Get the current time zone setting of the platform.
     */
    if (GetTimeZoneInformation(&tzi) == TIME_ZONE_ID_INVALID) {
        goto err;
    }

    /*
     * Determine if this is an NT system.
     */
    ver.dwOSVersionInfoSize = sizeof(ver);
    GetVersionEx(&ver);
    isNT = ver.dwPlatformId == VER_PLATFORM_WIN32_NT;

    /*
     * Determine if auto-daylight time adjustment is turned off. Also,
     * it has to get the correct standard time in case the name is too
     * long for GetTimeZoneInformation.
     */
    ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, NT_CURRENT_TZ_KEY, 0,
		       KEY_READ, (PHKEY)&hKey);
    if (ret != ERROR_SUCCESS) {
	ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, WIN_CURRENT_TZ_KEY, 0,
			   KEY_READ, (PHKEY)&hKey);
    }
    if (ret == ERROR_SUCCESS) {
	DWORD val;
	DWORD bufSize;

	valueType = 0;
	bufSize = sizeof(val);
	ret = RegQueryValueExA(hKey, "DisableAutoDaylightTimeSet", NULL,
			      &valueType, (LPBYTE) &val, &bufSize);
	if (ret == ERROR_SUCCESS && val == 1) {
	    int sign;
	    LONG gmtOffset;

	    (void) RegCloseKey(hKey);
	    if (tzi.Bias > 0) {
		gmtOffset = tzi.Bias;
		sign = -1;
	    } else {
		gmtOffset = -tzi.Bias;
		sign = 1;
	    }
	    if (gmtOffset != 0) {
		sprintf(winZoneName, "GMT%c%02d:%02d",
			((sign >= 0) ? '+' : '-'),
			gmtOffset / 60,
			gmtOffset % 60);
	    } else {
		strcpy(winZoneName, "GMT");
	    }
	    return VALUE_GMTOFFSET;
	}

	/*
	 * Win32 problem: If the length of the standard time name is equal
	 * to (or probably longer than) 32 in the registry,
	 * GetTimeZoneInformation() on NT returns a null string as its
	 * standard time name. We need to work around this problem by
	 * getting the same information from the TimeZoneInformation
	 * registry. The function on 98 seems to return its key name. We
	 * can't do anything in that case.
	 */
	if (tzi.StandardName[0] == 0) {
	    bufSize = sizeof(stdNameInReg);
	    ret = getValueInRegistry(hKey, STANDARD_NAME, &valueType,
				     (LPBYTE) stdNameInReg, &bufSize);
	    if (ret != ERROR_SUCCESS) {
		goto err;
	    }
	    stdNamePtr = stdNameInReg;
	}
	(void) RegCloseKey(hKey);
    }

    /*
     * Open the "Time Zones" registry.
     */
    ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, NT_TZ_KEY, 0, KEY_READ, (PHKEY)&hKey);
    if (ret != ERROR_SUCCESS) {
        ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, WIN_TZ_KEY, 0, KEY_READ, (PHKEY)&hKey);
        /*
	 * If both failed, then give up.
	 */
        if (ret != ERROR_SUCCESS) {
            return VALUE_UNKNOWN;
        }
    }

    /*
     * Get the number of subkeys of the "Time Zones" registry for
     * enumeration.
     */
    ret = RegQueryInfoKey(hKey, NULL, NULL, NULL, &nSubKeys,
                          NULL, NULL, NULL, NULL, NULL, NULL, NULL);
    if (ret != ERROR_SUCCESS) {
	goto err;
    }

    /*
     * Compare to the "Std" value of each subkey and find the entry that
     * matches the current control panel setting.
     */
    onlyMapID = 0;
    for (i = 0; i < nSubKeys; ++i) {
        DWORD size = sizeof(subKeyName);
        ret = RegEnumKeyEx(hKey, i, subKeyName, &size, NULL, NULL, NULL, NULL);
        if (ret != ERROR_SUCCESS) {
	    goto err;
        }
        ret = RegOpenKeyEx(hKey, subKeyName, 0, KEY_READ, (PHKEY)&hSubKey);
        if (ret != ERROR_SUCCESS) {
	    goto err;
        }

        size = sizeof(szValue);
        ret = getValueInRegistry(hSubKey, STD_NAME, &valueType,
				 szValue, &size);
        if (ret != ERROR_SUCCESS) {
	    /*
	     * NT 4.0 SP3 fails here since it doesn't have the "Std"
	     * entry in the Time Zones registry.
	     */
	    RegCloseKey(hSubKey);
            onlyMapID = 1;
	    ret = RegOpenKeyExW(hKey, stdNamePtr, 0, KEY_READ, (PHKEY)&hSubKey);
	    if (ret != ERROR_SUCCESS) {
		goto err;
	    }
            break;
	}

	if (wcscmp((WCHAR *)szValue, stdNamePtr) == 0) {
	    /*
	     * Some localized Win32 platforms use a same name to
	     * different time zones. So, we can't rely only on the name
	     * here. We need to check GMT offsets and transition dates
	     * to make sure it's the registry of the current time
	     * zone.
	     */
	    DWORD tziValueSize = sizeof(tempTzi);
	    ret = RegQueryValueEx(hSubKey, "TZI", NULL, &valueType,
				  (unsigned char *) &tempTzi, &tziValueSize);
	    if (ret == ERROR_SUCCESS) {
		if ((tzi.Bias != tempTzi.bias) ||
		    (memcmp((const void *) &tzi.StandardDate,
			    (const void *) &tempTzi.stdDate,
			    sizeof(SYSTEMTIME)) != 0)) {
			goto out;
		}

		if (tzi.DaylightBias != 0) {
		    if ((tzi.DaylightBias != tempTzi.dstBias) ||
			(memcmp((const void *) &tzi.DaylightDate,
				(const void *) &tempTzi.dstDate,
				sizeof(SYSTEMTIME)) != 0)) {
			goto out;
		    }
		}
	    }

            /*
	     * found matched record, terminate search
	     */
            strcpy(winZoneName, subKeyName);
            break;
        }
    out:
	(void) RegCloseKey(hSubKey);
    }

    /*
     * Get the "MapID" value of the registry to be able to eliminate
     * duplicated key names later.
     */
    valueSize = MAX_ZONE_CHAR;
    ret = RegQueryValueExA(hSubKey, "MapID", NULL, &valueType, winMapID, &valueSize);
    (void) RegCloseKey(hSubKey);
    (void) RegCloseKey(hKey);

    if (ret != ERROR_SUCCESS) {
        return VALUE_UNKNOWN;
    }

    return (onlyMapID == 1) ? VALUE_MAPID : VALUE_KEY;

 err:
    if (hKey != NULL) {
	(void) RegCloseKey(hKey);
    }
    return VALUE_UNKNOWN;
}

/*
 * The mapping table file name.
 */
#define MAPPINGS_FILE "\\lib\\tzmappings"

/*
 * Index values for the mapping table.
 */
#define TZ_WIN_NAME	0
#define TZ_MAPID	1
#define TZ_REGION	2
#define TZ_JAVA_NAME	3

#define TZ_NITEMS	4	/* number of items (fields) */

/*
 * Looks up the mapping table (tzmappings) and returns a Java time
 * zone ID (e.g., "America/Los_Angeles") if found. Otherwise, NULL is
 * returned.
 *
 * value_type is one of the following values:
 *	VALUE_KEY for exact key matching
 *	VALUE_MAPID for MapID and country-based mapping (this is
 *	required for the old Windows, such as NT 4.0 SP3).
 */
static char *matchJavaTZ(const char *java_home_dir, int value_type, char *tzName,
                         char *mapID, const char *country)
{
    int line;
    int IDmatched = 0;
    FILE *fp;
    char *javaTZName = NULL;
    char *items[TZ_NITEMS];
    char mapFileName[_MAX_PATH + 1];
    char lineBuffer[MAX_ZONE_CHAR * 4];
    char bestMatch[MAX_ZONE_CHAR];

    bestMatch[0] = '\0';

    strcpy(mapFileName, java_home_dir);
    strcat(mapFileName, MAPPINGS_FILE);

    if ((fp = fopen(mapFileName, "r")) == NULL) {
        jio_fprintf(stderr, "can't open %s.\n", mapFileName);
        return NULL;
    }

    line = 0;
    while (fgets(lineBuffer, sizeof(lineBuffer), fp) != NULL) {
        char *start, *idx, *endp;
        int itemIndex = 0;

        line++;
        start = idx = lineBuffer;
	endp = &lineBuffer[sizeof(lineBuffer)];

	/*
	 * Ignore comment and blank lines.
	 */
        if (*idx == '#' || *idx == '\n') {
            continue;
	}

	for (itemIndex = 0; itemIndex < TZ_NITEMS; itemIndex++) {
	    items[itemIndex] = start;
	    while (*idx && *idx != ':') {
		if (++idx >= endp) {
		    goto illegal_format;
		}
	    }
	    if (*idx == '\0') {
		goto illegal_format;
	    }
	    *idx++ = '\0';
	    start = idx;
	}

	if (*idx != '\n') {
	    goto illegal_format;
	}

        if (strcmp(mapID, items[TZ_MAPID]) == 0) {
            IDmatched = 1;
            if (strcmp(items[TZ_WIN_NAME], tzName) == 0) {
                /*
		 * Found the time zone in the mapping table.
		 */
		javaTZName = strdup(items[TZ_JAVA_NAME]);
		break;
            }
	    /*
	     * Try to find the most likely time zone.
	     */
	    if (*items[TZ_REGION] == '\0') {
		strncpy(bestMatch, items[TZ_JAVA_NAME], MAX_ZONE_CHAR);
	    } else if (country != NULL && strcmp(items[TZ_REGION], country) == 0) {
		if (value_type == VALUE_MAPID) {
		    javaTZName = strdup(items[TZ_JAVA_NAME]);
		    break;
		}
		strncpy(bestMatch, items[TZ_JAVA_NAME], MAX_ZONE_CHAR);
            }
        } else {
	    if (IDmatched == 1) {
		/*
		 * No need to look up the mapping table further.
		 */
		break;
	    }
        }
    }
    fclose(fp);

    if (javaTZName == NULL && bestMatch[0] != '\0') {
        javaTZName = strdup(bestMatch);
    }
    return javaTZName;

 illegal_format:
    (void) fclose(fp);
    jio_fprintf(stderr, "tzmappings: Illegal format at line %d.\n", line);
    return NULL;
}

/*
 * Detects the platform time zone which maps to a Java time zone ID.
 */
char *findJavaTZ_md(const char *java_home_dir, const char *country)
{
    char winZoneName[MAX_ZONE_CHAR];
    char winMapID[MAX_MAPID_LENGTH];
    char *std_timezone = NULL;
    int  result;

    result = getWinTimeZone(winZoneName, winMapID);

    if (result != VALUE_UNKNOWN) {
	if (result == VALUE_GMTOFFSET) {
	    std_timezone = strdup(winZoneName);
	} else {
	    std_timezone = matchJavaTZ(java_home_dir, result,
				       winZoneName, winMapID, country);
	}
    }

    return std_timezone;
}

/**
 * Returns a GMT-offset-based time zone ID. On Win32, it always return
 * NULL since the fall back is performed in getWinTimeZone().
 */
char *
getGMTOffsetID()
{
    return NULL;
}
